Day 27 把整條流程接成 CLI 和 endpoint,但成功回傳 JSON,不代表健康洞察說得對。ok 只表示通過現有檢查:引用可能不完整、資料不足時可能下了結論,也可能整篇都在免責,沒有回答問題。
Day 26 用九個測例比較 prompt-v1/v2,各跑三次,pass 分別是 8/27 與 16/27。不過,差距主要來自免責句的用詞、evidence.kind 標錯與一次截斷,還不能說 v2 的健康解讀比較正確。今天要重跑並擴大測例,拆開看:多通過了哪些檢查,又多了哪些錯?
這次沿用九個舊測例,再把合成情境換新 seed,新增十個,共 19 個;期望都在呼叫模型前固定。本輪只比 prompt-v1/v2,每版各跑三次,共 114 次呼叫。輸出上限改為 16,384 token,Day 26 在 8,192 上限下的結果另表對照;選例與設定細節放在 Hands-on。
合成情境記錄「我注入了什麼事件」,summary-v2 記錄「程式算出什麼、交給模型看什麼」。兩者可能不同:事件已注入,偏離分數卻沒有超過門檻。本篇評的是模型有沒有照可見資料回答,不能拿來證明它判對了人的生理狀態。
例如 S-008 注入了事件,summary 卻沒標旗,模型就不該說「程式已偵測到事件」。這條期望目前只寫在測例描述裡,還沒有對應的自動規則,仍需人工判讀。其他可檢查的期望則寫進測例檔,例如哪些指標不能下結論、哪些仍應回答。
舊九例多數已在先前實驗出現,prompt 與詞表也曾參照它們調整,適合拿來查舊錯是否重現。新測例則在看模型答案前固定期望;若看完答案才改規則,必須記錄版本,才能分清是模型改變,還是評分方式改變。
Faithfulness:引用能否支持整句話。 一筆 claim 的數值、日期、方向與判定狀態,都要有資料支持。path 存在只表示找得到來源,還得確認它支持了句中的哪些資訊。ALCE 用 citation recall 看陳述是否獲引用完整支持,用 citation precision 看每筆引用是否真的支持陳述。本篇的 faithfulness 接近前者,但單位是模型列出的 claim,沒有沿用論文的 NLI 模型評分。即使一句話與 summary 一致,也不代表 summary 本身正確;這是可驗證性與真實性的差別(Liu et al.)。
Evidence coverage:指定的欄位有沒有被引用。 本篇檢查 must_cite 指定的欄位或其子欄位,衡量引用範圍。「每句都有 evidence」已是驗證的基本要求,不另當成品質分數;有引用也不等於整句有依據。分母尤其要小心:Day 26 的 53 份可評分回應全通過 must_cite,但其中 23 份的清單是空的,實際有指定欄位可查的只有 30 份。空清單的 pass 不能算成功引用,也不能把欄位命中率當成主張獲支持的比例。
Abstention:該保留判斷的指標要保留,能回答的仍要回答。 abstain_scope 裡的指標不可判定,就不能說它相對 baseline 偏高、偏低或「沒有偏離」;must_answer 裡可判定的指標,仍須回答。全部拒答不算好成績。目前規則只查引用位置與部分方向詞,句意是否越界仍可能需要人看。
Hallucination:指出哪句話缺乏支持。 例如憑空補數字、把不可判定說成正常,或直接推論病因。判讀時要保留原句、引用與 summary,沿用 Day 24 的「支持、部分支持、不支持、不可解讀」四類;「無法判定」則留給資料狀態。部分支持可能只是引用不完整,不能和不支持混算。禁語命中也不等於 hallucination:「不是診斷」同樣含有「診斷」,因此詞表結果只是複核線索。
每份結果都要找得到測例、輸入、prompt 與檢查規則版本、輪次和失敗原因。截斷或 API 失敗沒有可評分內容,要與內容不合格分開;pass、needs_human、fail 也要分列,不能合成一個「正確率」。
規則能檢查格式、引用與部分用詞,整句是否獲支持仍需清楚的判準與可複核的判讀。Day 26 的人工確認是 0 列,沒有人工標註可驗證 LLM-as-judge,所以本輪不納入 judge。next_step 與 summary 標旗是否矛盾仍未裁決;evidence.kind 標錯則另外計數,但保留在 fail 裡,讓兩天的裁決維持同一口徑。若修規則,就升版,並分清舊紀錄重評與新模型呼叫。
檢查全過,也可能是檢查沒抓到錯。Day 26 的五項期望檢查沒有 fail,還需要用刻意改壞的輸出測試,例如換掉數字、拿掉不確定性,看規則能否發現。這輪尚未做這項對照。
舊九個(S-001~S-009)沿用 Day 26,期望不動。新十個用同一組 Day 14 情境設定、只換 seed(2801–2806)產生六條新序列,再由 days/day28/scan_candidates.py 依規則挑解讀日。規則在掃描資料前寫好,只有「事件已注入但沒標旗」這一條(S-016 用)是第一次掃描後補的,仍在呼叫模型前固定。
| 測例 | 類別 | 情境 | summary 的狀態 | 不能下結論 | 必須引用 |
|---|---|---|---|---|---|
| S-010 | normal | sleep_debt_d28 | 三項都沒標旗 | — | minutesAsleep |
| S-011 | normal | sync_delay_d28 | 三項都沒標旗 | — | rmssd |
| S-012 | normal | drift_vs_event_d28 | 三項都沒標旗,睡眠 +2.78 接近門檻 | — | minutesAsleep |
| S-013 | deviation | sleep_debt_d28 | resting_hr 標旗 up;rmssd 不可判定 | rmssd | resting_hr |
| S-014 | deviation | suspected_illness_d28 | resting_hr 標旗 up | — | resting_hr |
| S-015 | deviation | conflicting_rhr_down_d28 | resting_hr 標旗 down,rmssd −2.92 沒過門檻 | — | resting_hr |
| S-016 | deviation | drift_vs_event_d28 | 事件首日,但三項都沒標旗 | — | resting_hr |
| S-017 | insufficient | sleep_debt_d28 | 三項 baseline 都不足 | 三項 | (空) |
| S-018 | missing | conflicting_rhr_down_d28 | rmssd 當天缺值 | rmssd | minutesAsleep |
| S-019 | missing | mostly_missing_d28 | 一半的日子沒戴,三項都不可判定 | 三項 | (空) |
「三項」是 minutesAsleep、rmssd、resting_hr;「必須引用」指該指標的 deviation 欄位,解讀日與完整期望見 days/day28/results.md §2。新測例的必須引用用同一條規則:有標旗就引用標旗的指標,沒有就引用 |score| 最大的可判定指標。舊九個是 Day 26 逐例寫的,規則不同,所以後面的表都把舊、新分開。
有兩類沒補到。換 seed 後,原本設計成「兩項指標方向矛盾」的序列,rmssd 差一點沒過門檻(−2.92,門檻 3.0),60 天裡沒有一天是 conflicting;模擬同步延遲的序列也沒有誤報日。效果量貼著門檻時,有沒有標旗要看 seed。
期望先對照重新產生的 summary 逐一核對,commit 凍結(357353e)後才加入 runner,再開始呼叫模型。tests/test_day28.py 可以重驗期望與輸入一致,但這不證明期望本身寫對。
v1 與 v2 只差 prompt 的規則 9(src/wearable_ai/ai/prompts.py)。輸入、輸出格式、模型(gemini-3.8-flash,temperature 1.0)都和 Day 26 相同,唯一改的是輸出上限:Day 25、26 在 8,192 token 下都出現 thinking 吃完額度的截斷,這次提高到 16,384。
19 測例 × 2 版 × 3 輪 = 114 次。同一測例的 v1、v2 接著跑,先後順序每輪交替,避免某一版固定排前面。
評分沿用 evalcheck-v2,看完結果也沒有改。檢查分三層:有沒有完整回應 → 結構與引用契約(schema、evidence_exists、cross_field)→ 測例期望(must_cite、abstain_scope、must_answer、forbidden_phrases、max_claims),裁決取最嚴的一項。表格口徑在跑之前定好:每張表寫分母;evidence.kind 標錯另外計數,但仍算 fail;空的必須引用清單不算成功引用;舊、新測例分開,Day 26 的 54 次只另表對照。
這輪沒有人工判讀,沒有 judge,也還沒做「刻意改壞的輸出」對照。
呼叫順序(days/day28/eval_run.py):
def plan(spec_ids, runs=RUNS, first_run=1):
"""每輪依序跑每個測例;同一測例的 v1、v2 相鄰,誰先依 (輪次 + 測例序號) 的奇偶交替。"""
todo = []
for k in range(first_run, first_run + runs):
for i, sid in enumerate(spec_ids):
order = VERSIONS if (k + i) % 2 == 0 else VERSIONS[::-1]
todo += [(group_name(sid, v), k) for v in order]
return todo
evalcheck 每份只列前 3 筆 kind 錯誤,所以計數改讀驗證器 grounding.validate_output 的完整清單(days/day28/results_tables.py 的 kind_errors)。重現:
uv run python days/day28/eval_run.py --section 1 # 組輸入、核對期望,不呼叫 API
uv run python days/day28/eval_run.py --section 2 --runs 3 # 114 次呼叫
uv run python days/day28/eval_run.py --section 3 # 評分,寫 results.csv
uv run python days/day28/results_tables.py # 重產 results.md
114 次都是第一次就成功、沒有截斷,耗時中位數 v1 18.0 秒、v2 22.0 秒。不過最大的一筆輸出加 thinking 用了 16,020 token,是上限的 98%;有 5 筆超過 Day 26 的上限 8,192(days/day28/results.md §1,超過 8,192 的筆數由 runs/*.json 重算)。
| cohort | prompt | 次數 | pass | needs_human | fail |
|---|---|---|---|---|---|
| 舊九個 | prompt-v1 | 27 | 6 | 18 | 3 |
| 舊九個 | prompt-v2 | 27 | 8 | 10 | 9 |
| 新十個 | prompt-v1 | 30 | 12 | 12 | 6 |
| 新十個 | prompt-v2 | 30 | 14 | 12 | 4 |
| 合計 | prompt-v1 | 57 | 18 | 30 | 9 |
| 合計 | prompt-v2 | 57 | 22 | 22 | 13 |
114 份都可評分,沒有截斷或 API 失敗。逐項拆開:
| 檢查 | v1 pass/nh/fail/skipped | v2 pass/nh/fail/skipped |
|---|---|---|
schema、evidence_exists |
57/0/0/0 | 57/0/0/0 |
cross_field |
50/0/7/0 | 44/0/13/0 |
must_cite |
57/0/0/0 | 57/0/0/0 |
abstain_scope |
20/0/1/36 | 21/0/0/36 |
must_answer |
45/0/0/12 | 45/0/0/12 |
forbidden_phrases |
23/33/1/0 | 28/29/0/0 |
max_claims |
57/0/0/0 | 57/0/0/0 |
22 個 fail 裡有 20 個是 cross_field,另外 2 個都在 v1(結果四)。v2 比 v1 多 4 個 pass,也多 4 個 fail;只看 pass 會漏掉後一半。
同一格(測例 × 版本)跑三次,結果也常不一樣。v2 只有 S-008 三次都 fail,v1 沒有這樣的格子(days/day28/results.md §4)。
舊九個的輸入、prompt 與評分規則都和 Day 26 相同,差在呼叫日期與輸出上限(days/day28/results.md §9):
| 條件 | prompt | pass | needs_human | fail | truncated | kind 標錯(紀錄/筆) |
|---|---|---|---|---|---|---|
| Day 26(8,192) | v1 | 8 | 13 | 5 | 1 | 5/13 |
| Day 26(8,192) | v2 | 16 | 8 | 3 | 0 | 3/7 |
| Day 28(16,384) | v1 | 6 | 18 | 3 | 0 | 3/9 |
| Day 28(16,384) | v2 | 8 | 10 | 9 | 0 | 9/21 |
每列 27 次。v2 的 pass 從 16 變 8,fail 從 3 變 9,多出來的都是 kind 標錯;v1 變化小得多。v2 的 pass 仍比 v1 多,但差距從 8 份縮成 2 份,fail 則從較少變成較多。上限和日期同時改變,每格又只有三次,這張表分不出原因。
kind 標錯:20 份、48 筆。 引用的欄位都存在,錯在把它的類別標成 data_note(資料備註)(days/day28/results.md §5):
| 被引用的欄位 | 正確的 kind | v1 筆數 | v2 筆數 |
|---|---|---|---|
baseline.min_obs_for_center |
program_judgement | 11 | 9 |
daily[n].missing_kind |
observation | 3 | 14 |
cross_metric.category、pattern |
program_judgement | 0 | 6 |
daily[n].date |
observation | 3 | 0 |
deviation.reason |
program_judgement | 2 | 0 |
前兩類 Day 26 也出現過。第一類出在資料不足的說明,例如 S-019:「僅有 6 筆觀測值……未達計算中心值所需的 14 筆」,同一句其他引用都標對,只有「需要 14 筆」這個門檻標成資料備註。第二類出在描述缺值日;v2 的 14 筆裡有 9 筆來自 S-008 的三輪,都是同一種句子,所以 v2 這類多,主要集中在一格。
abstain_scope 的 1 個 fail(S-017 v1)。 三項都要保留判斷,模型寫「所有指標目前皆無法判定」,句意沒錯,但引用只指到睡眠,沒有指到 rmssd 與 resting_hr。規則要求逐項引用,所以判 fail。
forbidden_phrases 的 1 個 fail(S-011 v1)。 句子是免責句「……非臨床診斷量測值」,但「非」不在否定詞表,命中「診斷」就判 fail。83 個禁語命中裡,其餘 82 個前面都有否定詞,送人工複核。這和 Day 26 的「缺乏」是同一種漏洞;評分規則這輪固定不改,這個 fail 照算。
排除 36 份空清單後,78 份回應都引用了指定欄位,舊、新測例都是 100%(days/day28/results.md §6)。和 Day 26 一樣,這項檢查沒有挑出問題。
Day 24 的逐句 checker(不計入裁決)把兩版各約 575 筆 claim 中約七成判為 needs_human(v1 401/574、v2 402/579,days/day28/results.md §8)。這輪沒有人看。
| 原本以為 | 實際發現 |
|---|---|
| 我原本以為,沿用同九個測例與評分規則重跑,v2 的 pass 數會接近 Day 26 的 16/27,對 v1 的優勢也會相近。 | 同樣九個測例、同樣 prompt 與檢查規則,v2 的 pass 從 Day 26 的 16/27 變成 8/27,fail 從 3 變 9;v2 對 v1 的 pass 差距從 8 份縮成 2 份,fail 由較少變成較多。 |
| 我原本以為,v2 的規則 9 把不可判定範圍說得更明確,會讓更多輸出通過,而不會同時增加硬性違規。 | v2 合計比 v1 多 4 個 pass、也多 4 個 fail(22 vs 18、13 vs 9),少 8 個 needs_human。 |
| 我原本以為,新增的指標層級期望會比通用結構檢查更能挑出測例特有的錯;kind 標錯不會主導 fail。 | 22 個 fail 有 20 個是 kind 標錯(48 筆,全標成 data_note);測例指定的引用、拒答與必答檢查只抓到 1 個,而且是引用位置不符、句意沒錯;共用禁語詞表另有 1 個已知誤判。 |
| 我原本以為,Day 26 已補過「缺乏」這個否定詞,免責句被當成違規的問題在這輪應該較少出現。 | 唯一的 forbidden_phrases fail 是免責句「非臨床診斷量測值」,因為「非」不在否定詞表;和 Day 26 的「缺乏」同一種漏洞。 |
| 我原本以為,把輸出上限從 8,192 提到 16,384,足以消除截斷,也會留下充裕的餘量。 | max_output_tokens 16384 下 114 次沒有截斷,但最大一筆用到上限的 98%,5 筆超過 Day 26 的上限 8,192。 |
我原本以為,排除空清單後,must_cite 至少會抓到一些漏引指定欄位的回應;新測例的選取規則也能提供辨別力。 |
必要欄位引用覆蓋率 78/78;舊、新兩套 must_cite 規則都沒挑出問題。 |
這輪比較只有一個模型、固定的一句問題,19 個測例在每版各跑三次;這些次數可呈現本批輸出的波動,還不足以把 pass 數差異當成穩定的版本效果。evalcheck-v2 只檢查預設的結構、引用與部分期望,沒有判 next_step 是否與標旗矛盾(已知案例 C16.n1);也沒有用刻意改壞的輸出測試規則能抓到多少錯。逐句 checker 約七成 claim 是 needs_human,本輪人工確認為零,因此 78/78 的指定欄位引用與自動裁決都不能當成逐句忠實度或 hallucination 的人工驗證。詞表把「非臨床診斷量測值」判成 fail,說明裁決仍含已知的字面誤判。舊九個測例多數在 Day 20–26 已看過,prompt 與詞表曾參照它們調整;Day 26 與本輪又同時相隔兩天並把 max_output_tokens 從 8,192 提到 16,384,無法從兩張結果表分辨 v2 變化的原因。
資料是合成情境及其產生的 summary-v2,評測期望也依這份 summary 訂定;結果只表示輸出是否符合模型可見資料和現有檢查,不能推論真實穿戴量測、人的生理狀態或醫療判斷準確率。19 個測例來自 13 條序列,新六條雖換了 seed,仍沿用舊情境的生成設定,雜訊可獨立而產生過程不獨立,不能把 114 次呼叫視為 114 個獨立情境。事件效果量靠近偵測門檻時,換 seed 便可能改變 summary 的標旗:這批沒有第二個 contradictory 測例,sync_delay_d28 也沒有誤報日,因此相關邊界沒有被新測例覆蓋;S-016 的事件雖已注入,summary 未標旗,本篇只能檢查模型有沒有忠於未標旗的輸入。
第一,回歸結果要能重現才算結論。Day 26 的 v2 優勢,這次在同樣九個測例上只剩一小段差距;如果當時就據此選定 v2,等於把一次抽樣當成性質。temperature 1.0 下每格跑 3 次,只能記錄這批有沒有出現、出現幾次,不足以估計發生率或版本差異。要比較兩個版本,次數、條件與改了哪個變數都得先寫清楚,結果不一致時才知道該懷疑什麼。
第二,pass 變多不等於變好。pass、needs_human、fail 要一起報,再拆到逐項與逐格,才看得到 fail 也跟著變多。
第三,fail 多半來自共用的結構契約,不是測例專屬的期望。kind 標錯不需要 ground truth 就抓得到;測例期望目前主要檢查引用結構與有限的字面規則,抓不到「引用了但說錯」。要知道規則抓不抓得到錯,下一步是用刻意改壞的輸出當對照,而不是再加測例。
最後,凍結評分規則有代價,也有用處。「非」的漏洞是看到輸出才發現的,但這次沒有當場改詞表,兩天的裁決才是同一把尺,能直接對照。要修就升版、把兩天的紀錄都重評,差異另表列出。